/*
 * @file iso9660_record.c
 *
 *    ISO9660 records management.
 */

#include <stdio.h>
#include <inttypes.h>
#include <errno.h>
#include <string.h>
#include <malloc.h>

#include <rtems.h>
#include <rtems/seterr.h>

#include "iso9660.h"
#include "iso9660_io.h"
#include "iso9660_susp.h"
#include "iso9660_record.h"
#include "iso9660_handlers.h"

/**
 *   Convert a ISO9660 directory/file record date to a standard C timespec.
 * The nsec member of the timespec structure will always be initialized to
 * zero.
 *
 * @param d Pointer to the iso9660_date_t date member of the record.
 *
 * @return A timespec structure equal describing the same time value.
 */
struct timespec
iso9660_record_date_to_timespec(
    iso9660_date_t  *d
)
{
  struct tm t;
  struct timespec r;

  t.tm_sec = d->second;
  t.tm_min = d->minute;
  t.tm_hour = d->hour;
  t.tm_mday = d->day;
  t.tm_mon = d->month - 1;
  t.tm_year = d->year;

  r.tv_sec = mktime(&t);
  r.tv_nsec = 0;

  return r;
}

int
bcd_to_dec(
    char *bcd,
    int size
)
{
  int i, n, result;

  for (i = 0, n = 1, result = 0; i < size; i++, n *= 10)
    result += bcd[size-i-1] * n;

  return result;
}

struct timespec
iso9660_record_long_date_to_timespec(
    iso9660_bcd_date_t  *d
)
{
  struct tm t;
  struct timespec r;

  t.tm_sec = bcd_to_dec(d->second, 2);
  t.tm_min = bcd_to_dec(d->minute, 2);
  t.tm_hour = bcd_to_dec(d->hour, 2);
  t.tm_mday = bcd_to_dec(d->day, 2);
  t.tm_mon = bcd_to_dec(d->month, 2);
  t.tm_year = bcd_to_dec(d->year, 4);

  r.tv_sec = mktime(&t);
  r.tv_nsec = 0;

  return r;
}

int iso9660_record_init_identifier(
    iso9660_directory_record_t  *dir_record,
    iso9660_record_t            *record
)
{
  int rc;
  char *name;

  /* Handle special filenames */
  record->name_len = ISO9660_RECORD_LEN_FI(dir_record);
  name = ISO9660_RECORD_FILE_IDENTIFIER(dir_record);

  /* Special  'current' directory */
  if ((record->name_len == 1) && (name[0] == 0))
  {
    record->name_len = 1;
    record->name = calloc(1, sizeof(char));
    if (record->name == NULL)
      rtems_iso9660_error_and_return_minus_one(ENOMEM,
      "record_create: Failed to allocate memory for new record name.");

    memcpy(record->name, ".", 1);
    return 0;
  }

  /* Special 'parent' directory */
  if ((record->name_len == 1) && (name[0] == 1))
  {
    record->name_len = 2;
    record->name = calloc(2, sizeof(char));
    if (record->name == NULL)
      rtems_iso9660_error_and_return_minus_one(ENOMEM,
      "record_create: Failed to allocate memory for new record name.");

    memcpy(record->name, "..", 2);
    return 0;
  }


  /* Step 1 : Check if Rock Ridge should be used (only if Joliet is not available) */
  if (iso9660_version_supports(record->fs->version, ISO9660_EXTENSION_ROCK_RIDGE)
      && record->system_use_size > 0)
  {
    iso9660_susp_list_t susp_list;
    iso9660_susp_entry_t *susp_entry;

    rtems_iso9660_dbg_trace(RTEMS_ISO9660_DEBUG_RECORD_CREATE,
        "Using SUSP/RRIP NM record for alternate name.\n");

    rc = iso9660_susp_entry_open_record(record, &susp_list);
    if (rc == -1)
    {
      rtems_iso9660_dbg_trace(RTEMS_ISO9660_DEBUG_RECORD_CREATE,
          "Failed to init SUSP list, falling back to ISO9660 name.\n");
      goto fallback;
    }

    /* Looking up the NM entry */
    record->name = NULL;
    rc = iso9660_susp_list_search(&susp_entry, ISO9660_SUSP_SIGNATURE_NM, &susp_list);
    if (rc == RTEMS_SUCCESSFUL)
    {
      record->name_len = ISO9660_SUSP_NM_NAME_LEN(susp_entry);

      record->name = calloc(record->name_len, sizeof(char));
      if (record->name == NULL)
        rtems_iso9660_error_and_return_minus_one(ENOMEM,
        "record_create: Failed to allocate memory for new record name.");

      memcpy(record->name, ISO9660_SUSP_NM_NAME(susp_entry), record->name_len);
    }

    iso9660_susp_list_free(&susp_list);

    /* No NM entry in the SUSP records */
    if (record->name == NULL)
    {
      rtems_iso9660_dbg_trace(RTEMS_ISO9660_DEBUG_RECORD_CREATE,
          "Unable to retrieve SUSP/RRIP NM entry, falling back to ISO9660 name.\n");
      goto fallback;
    }

    return 0;
  }


  /* Use the classic name as a fallback */
fallback:
  if (!record->flags.directory)
    record->name_len = iso9660_clean_identifier_length(record->fs, name, record->name_len);

  rtems_iso9660_dbg_trace(RTEMS_ISO9660_DEBUG_RECORD_CREATE,
      "Using ISO9660 name.\n");

  record->name = calloc(record->name_len, sizeof(char));
  if (record->name == NULL)
    rtems_iso9660_error_and_return_minus_one(ENOMEM,
    "record_create: Failed to allocate memory for new record name.");

  memcpy(record->name, name, record->name_len);

  return 0;
}

iso9660_record_t* iso9660_record_create_from_extent(
    iso9660_extent_t    extent,
    iso9660_fs_info_t   *fs
)
{
  int rc;
  char sector_buffer[ISO9660_SECTOR_SIZE];

  rc = iso9660_read_sector(fs, fs->volume.root_record_location, 0,
      ISO9660_SECTOR_SIZE, sector_buffer);
  if (rc < 0)
    rtems_iso9660_error_and_return_null(EIO,
      "record_create_from_extent: Failed to read extent.");

  return iso9660_record_create((iso9660_directory_record_t*)sector_buffer, extent, fs);
}


/**
 *   Create a ISO9660 record to use among the various record
 * management functions from a standard ISO9660 directory record.
 *
 * TODO: Use a circular list to cache the created records.
 *
 * @param dir_record  Pointer to a standard iso9660 directory record
 * @param fs          Filesystem information structure pointer
 *
 * @return Either NULL if the creation failed or a pointer to the created
 *         record descriptor.
 */
iso9660_record_t* iso9660_record_create(
    iso9660_directory_record_t *dir_record,
    iso9660_sector_t            extent,
    iso9660_fs_info_t          *fs
)
{
  iso9660_record_t *result;
  int rc;

  result = calloc(1, sizeof(iso9660_record_t));
  if (result == NULL)
    rtems_iso9660_error_and_return_null(ENOMEM,
        "record_create: Failed to allocate memory for new record.");

  /* Calculate the system use area size for SUSP */
  result->system_use = NULL;
  result->system_use_size = ISO9660_RECORD_SYSTEM_USE_LEN(dir_record);

  /* Copy this area in memory if available */
  if (result->system_use_size > 0)
  {
    rtems_iso9660_dbg_trace(RTEMS_ISO9660_DEBUG_RECORD_CREATE,
        "Allocating space for a %d bytes system use area.\n", result->system_use_size);
    result->system_use = malloc(result->system_use_size * sizeof(char));

    if (result->system_use == NULL)
    {
      free(result);
      rtems_iso9660_error_and_return_null(ENOMEM,
          "record_create: Failed to allocate memory for new record system use area.");
    }

    memcpy(result->system_use, ISO9660_RECORD_SYSTEM_USE(dir_record),
        result->system_use_size);
  }

  result->fs = fs;
  result->record_len = ISO9660_RECORD_LEN(dir_record);
  result->location = ISO9660_RECORD_LOCATION(dir_record);
  result->size = ISO9660_RECORD_DATA_LENGTH(dir_record);
  result->flags.existence = (ISO9660_RECORD_FLAGS(dir_record) & ISO9660_RECORD_FLAG_EXISTENCE)?1:0;
  result->flags.directory = (ISO9660_RECORD_FLAGS(dir_record) & ISO9660_RECORD_FLAG_DIRECTORY)?1:0;
  result->flags.associated = (ISO9660_RECORD_FLAGS(dir_record) & ISO9660_RECORD_FLAG_ASSOCIATED)?1:0;
  result->flags.record = (ISO9660_RECORD_FLAGS(dir_record) & ISO9660_RECORD_FLAG_RECORD)?1:0;
  result->flags.protection = (ISO9660_RECORD_FLAGS(dir_record) & ISO9660_RECORD_FLAG_PROTECTION)?1:0;
  result->flags.multiextent = (ISO9660_RECORD_FLAGS(dir_record) & ISO9660_RECORD_FLAG_MULTIEXTENT)?1:0;
  result->cache = NULL;
  result->extent = extent;
  result->date = iso9660_record_date_to_timespec(ISO9660_RECORD_DATE(dir_record));

  /* Initialize the record identifier */
  rc = iso9660_record_init_identifier(dir_record, result);

  if (rc < 0)
  {
    free(result->system_use);
    free(result);

    rtems_iso9660_error_and_return_null(ENOMEM,
      "record_create: Failed to init new record name.");
  }

  return result;
}


/**
 *   Make a deep copy of the record pointed out by source in
 * the record pointed out by dest. If the cache was opened in
 * source, its use counter is incremented.
 *
 * @param dest   Destination record
 * @param source Source record
 *
 * @return 0 is the copy is OK, else -1
 */
int iso9660_record_copy(
    iso9660_record_t  *dest,
    iso9660_record_t  *source
)
{
  dest->fs = source->fs;
  dest->location = source->location;
  dest->size = source->size;
  dest->name_len = source->name_len;
  dest->flags = source->flags;
  dest->extent = source->extent;
  dest->date = source->date;

  dest->name = malloc(source->name_len * sizeof(char));
  if (dest->name == NULL)
    rtems_iso9660_error_and_return_minus_one(ENOMEM,
        "record_copy: Failed to allocate memory for copy record name.");
  memcpy(dest->name, source->name, source->name_len);

  dest->system_use = NULL;
  dest->system_use_size = source->system_use_size;

  if (dest->system_use_size > 0)
  {
    dest->system_use = calloc(1, dest->system_use_size);

    if (dest->system_use == NULL)
    {
      free(dest->name);
      rtems_iso9660_error_and_return_minus_one(ENOMEM,
          "record_copy: Failed to allocate memory for"
          "new record system use area.");
    }

    memcpy(dest->system_use, source->system_use,
        dest->system_use_size);
  }

  dest->cache = source->cache;
  /* Increment the use counter of the cache if needed */
  if (dest->cache != NULL)
    iso9660_record_open(dest);

  return 0;
}


/**
 *    Free the memory allocated by iso9660_record_create. If the record
 * hasn't been closed and the cache is still held the call will fail.
 *
 * @param record A pointer to the record descriptor which should be freed
 *
 * @return 0 if sucessful, else -1
 */
int iso9660_record_free(
    iso9660_record_t    *record
)
{
  if (record == NULL)
    return 0;

  if (record->cache != NULL)
    iso9660_record_close(record);

  free(record->system_use);
  free(record->name);
  return 0;
}


/**
 *     Open a record descriptor to allow directory record traversal or read
 *  operations on a file. This function loads the corresponding sector in memory
 *  to avoid further disk access when accessing the list of records in case of a
 *  directory.
 *
 *  TODO: Use a global cache for all sectors
 *
 *  @param record  A pointer to the record descriptor to "open"
 *
 *  @return 0 if opening is successful, else -1
 */
int iso9660_record_open(
    iso9660_record_t    *record
)
{
  /* Create the cache or increment the cache use counter */
  if (record->cache == NULL)
  {
    int bytes;

    record->cache = calloc(1, sizeof(iso9660_record_cache_t));
    if (record->cache == NULL)
      rtems_iso9660_error_and_return_minus_one(ENOMEM,
          "record_open: Failed to allocate memory for cache.");

    record->cache->buffer = calloc(1, ISO9660_SECTOR_SIZE);
    if (record->cache->buffer == NULL)
    {
      free(record->cache);
      record->cache = NULL;
      rtems_iso9660_error_and_return_minus_one(ENOMEM,
          "record_open: Failed to allocate memory for cache buffer.");
    }

    record->cache->sector = record->location;
    record->cache->count = ISO9660_SECTOR_SIZE;
    record->cache->use = 1;
    record->cache->offset = 0;

    bytes = iso9660_read_sector(record->fs, record->location, 0, ISO9660_SECTOR_SIZE, record->cache->buffer);
    if (bytes != ISO9660_SECTOR_SIZE)
    {
      free(record->cache->buffer);
      free(record->cache);
      record->cache = NULL;

      {
        char buffer[128];

#if RTEMS_ISO9660_DEBUG
        snprintf(buffer, 127,
            "record_open: Unable to read sector %" PRIu32 ".",
            (uint32_t)record->location);
#endif

        rtems_iso9660_error_and_return_minus_one(ENOMEM, buffer);
      }
    }
  }
  else
  {
    record->cache->use++;
  }


  return 0;
}


/**
 *   Fetch the next entry in a directory record list.
 *
 * @param record  A pointer to the record descriptor
 * @param offset  Offset in the record list returned by a previous
 *                call to this function
 * @param entry   Fetched entry in the directory (NULL if there is no more entries)
 * @param flags   A set of flags to tweak the function :
 *                   - ISO9660_RECORD_SKIP_CURRENT : Skip the "current" (.) entry
 *                   - ISO9660_RECORD_SKIP_PARENT : Skip the "parent" (..) entry
 *
 * @return 0 if there is no more entries in the directory record,
 *         -1 if an error occured,
 *         an offset for the next entry if everything is ok
 */
int iso9660_record_next_entry(
    iso9660_record_t    *record,
    uint16_t             offset,
    iso9660_record_t    *output,
    uint8_t              flags
)
{
  iso9660_record_t            *entry;
  iso9660_directory_record_t  *iso_record;

  if (record->cache == NULL)
    rtems_iso9660_error_and_return_minus_one(EINVAL,
        "record_next: Uninitialized cache. Record not opened ?");

  iso_record = (iso9660_directory_record_t*)(record->cache->buffer + offset);

  /* Directory end */
  if (ISO9660_RECORD_LEN(iso_record) == 0)
    return 0;

  entry = iso9660_record_create(iso_record, record->location, record->fs);
  if (entry == NULL)
    rtems_iso9660_error_and_return_minus_one(errno,
        "record_next: Failed to create record.");

  offset += entry->record_len;

  /* Skip parent and/or current directory if needed */
  if (
      ((flags & ISO9660_RECORD_SKIP_PARENT) && iso9660_record_is_parent(entry)) ||
      ((flags & ISO9660_RECORD_SKIP_CURRENT) && iso9660_record_is_current(entry))
     )
  {
    iso9660_record_free(entry);
    free(entry);
    return iso9660_record_next_entry(record, offset, output, flags);
  }

  iso9660_record_copy(output, entry);
  iso9660_record_free(entry);
  free(entry);
  return offset;
}


/**
 *    Ensure that the record descriptor isn't used by anyone else and
 *  free the previously allocated cache.
 *
 *  @param record  A pointer to the record descriptor to "close"
 *
 *  @return 0 if the record is correctly closed, else -1
 */
int iso9660_record_close(
    iso9660_record_t    *record
)
{
  if (record->cache == NULL)
    rtems_iso9660_error_and_return_minus_one(EINVAL,
        "record_close: Uninitialized cache. Record not opened ?");

  record->cache->use--;

  if (record->cache->use == 0)
  {
    free(record->cache->buffer);
    free(record->cache);
    record->cache = NULL;
  }

  return 0;
}


#define tm_update(_flag, _var)                                                                       \
  if (tf_data->flags._flag) {                                                                         \
    if (_var != NULL) {                                                                               \
      unsigned char *ptr = susp_entry.data + 1 + tf_offset;                                          \
      *_var = (tf_data->flags.long_form) ?                                                             \
                    iso9660_record_long_date_to_timespec((iso9660_bcd_date_t*)ptr)                     \
                  : iso9660_record_date_to_timespec((iso9660_date_t*)ptr);                             \
    }                                                                                                  \
    tf_offset += (tf_data->flags.long_form) ? sizeof(iso9660_bcd_date_t) : sizeof(iso9660_date_t);   \
  }                                                                                                   \
  else if (_var != NULL)                                                                              \
    *_var = record->date

int
iso9660_record_extract_timestamp(
    iso9660_record_t *record,
    struct timespec *creation,
    struct timespec *access,
    struct timespec *modification,
    struct timespec *attribute,
    struct timespec *backup,
    struct timespec *effective
)
{
  iso9660_susp_list_t susp_list;
  iso9660_susp_entry_t *susp_entry;
  int rc;
  /*int tf_offset = 0;*/

  if (record->system_use_size <= 0)
    goto fallback_time;

  rc = iso9660_susp_entry_open_record(record, &susp_list);
  if (rc == -1)
  {
    rtems_iso9660_dbg_trace(RTEMS_ISO9660_DEBUG_STAT,
        "Failed to init SUSP list, falling back to ISO9660 data.\n");
    goto fallback_time;
  }

  rc = iso9660_susp_list_search(&susp_entry, ISO9660_SUSP_SIGNATURE_TF, &susp_list);
  if (rc != RTEMS_SUCCESSFUL)
  {
    iso9660_susp_list_free(&susp_list);
    goto fallback_time;
  }

  /* Timestamps are stored in the order of appearance of the flags,
   * it's imperative to keep this order !
   */
  /*
  tm_update(creation, creation);
  tm_update(modify, modification);
  tm_update(access, access);
  tm_update(attribute, attribute);
  tm_update(backup, backup);
  tm_update(effective, effective);*/

  iso9660_susp_list_free(&susp_list);
  return 0;

fallback_time:
  if (creation != NULL)
    *creation = record->date;

  if (access != NULL)
    *access = record->date;

  if (modification != NULL)
    *modification = record->date;

  if (attribute != NULL)
    *attribute = record->date;

  if (backup != NULL)
    *backup = record->date;

  return 0;
}


int iso9660_record_stat(
    iso9660_record_t    *record,
    struct stat        *buf,
    char                dir
)
{
  int rc;
  iso9660_susp_list_t susp_list;
  iso9660_susp_entry_t *susp_entry;

  /* No rock ridge support for this record */
  if (record->system_use_size <= 0)
    goto fallback_px;

  rc = iso9660_susp_entry_open_record(record, &susp_list);
  if (rc == -1)
  {
    rtems_iso9660_dbg_trace(RTEMS_ISO9660_DEBUG_STAT,
        "Failed to init SUSP list, falling back to ISO9660 data.\n");
    goto fallback_px;
  }

  /* Looking up the PX entry */
  rc = iso9660_susp_list_search(&susp_entry, ISO9660_SUSP_SIGNATURE_PX, &susp_list);
  if (rc == RTEMS_SUCCESSFUL)
  {
    buf->st_ino = (ino_t)ISO9660_SUSP_PX_INO(susp_entry);
    buf->st_size = (size_t)record->size;
    buf->st_uid = (uid_t)ISO9660_SUSP_PX_UID(susp_entry);
    buf->st_gid = (gid_t)ISO9660_SUSP_PX_GID(susp_entry);
    buf->st_mode =(mode_t)ISO9660_SUSP_PX_MODE(susp_entry);
    buf->st_nlink = (nlink_t)ISO9660_SUSP_PX_NLINKS(susp_entry);
    buf->st_dev = record->fs->dev;
  }
  iso9660_susp_list_free(&susp_list);

  if (rc == RTEMS_SUCCESSFUL)
    goto time;

fallback_px:
  /* Fallback data for POSIX stuff, its always available */
  buf->st_ino = (ino_t)record->location;
  buf->st_size = (size_t)record->size;
  buf->st_dev = record->fs->dev;
  buf->st_mode = (dir?S_IFDIR:S_IFREG) | S_IRUSR | S_IXUSR;

time:
  iso9660_record_extract_timestamp(record, &buf->st_ctim, &buf->st_mtim, &buf->st_atim, NULL, NULL, NULL);

  return 0;
}
